Skip to content

Conversation

@Hanumanth04
Copy link
Contributor

@Hanumanth04 Hanumanth04 commented Oct 16, 2025

Runtime verification on Linalg structured ops unconditionally computed end - 1 to determine the last iteration index before composing indexing maps. This caused spurious "negative index" assertion failures while operating on empty tensors (tensors with a dimension of size 0).

The issue occurs because:

  1. Empty tensors create loop ranges [0, 0) with zero trip count

  2. Computing end - 1 = 0 - 1 = -1 creates a fictitious negative index

  3. The negative index check triggers even though no loop iterations occur

The fix is to guard all runtime verification with a check that ensures all loop ranges are non-empty (start < end) before performing any index arithmetic.

Example MLIR that previously failed:

func.func @fill_empty() -> tensor<0xi32> {
  %c0 = arith.constant 0 : i32
  %empty = tensor.empty() : tensor<0xi32>
  %filled = linalg.fill ins(%c0 : i32) outs(%empty : tensor<0xi32>) -> tensor<0xi32>
  return %filled : tensor<0xi32>
}

@llvmbot
Copy link
Member

llvmbot commented Oct 16, 2025

@llvm/pr-subscribers-mlir-linalg

Author: Hanumanth (Hanumanth04)

Changes

[mlir][linalg] Fix Linalg runtime verification pass to handle tensors with dimensions of size 0

Runtime verification on Linalg structured ops unconditionally computed end - 1 to determine the last iteration index before composing indexing maps. This caused spurious "negative index" assertion failures while operating on empty tensors (tensors with a dimension of size 0).

The issue occurs because:

  1. Empty tensors create loop ranges [0, 0) with zero trip count

  2. Computing end - 1 = 0 - 1 = -1 creates a fictitious negative index

  3. The negative index check triggers even though no loop iterations occur

The fix is to guard all runtime verification with a check that ensures all loop ranges are non-empty (start < end) before performing any index arithmetic.

Example MLIR that previously failed:

func.func @<!-- -->fill_empty() -&gt; tensor&lt;0xi32&gt; {
  %c0 = arith.constant 0 : i32
  %empty = tensor.empty() : tensor&lt;0xi32&gt;
  %filled = linalg.fill ins(%c0 : i32) outs(%empty : tensor&lt;0xi32&gt;) -&gt; tensor&lt;0xi32&gt;
  return %filled : tensor&lt;0xi32&gt;
}

Full diff: https://github.com/llvm/llvm-project/pull/163791.diff

2 Files Affected:

  • (modified) mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp (+29-2)
  • (modified) mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir (+11)
diff --git a/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp b/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp
index 15eb51a6dcab2..737652c8cb9d1 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp
@@ -17,6 +17,7 @@
 #include "mlir/Dialect/Index/IR/IndexOps.h"
 #include "mlir/Dialect/Linalg/IR/Linalg.h"
 #include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/SCF/IR/SCF.h"
 #include "mlir/Dialect/Tensor/IR/Tensor.h"
 #include "mlir/Interfaces/RuntimeVerifiableOpInterface.h"
 
@@ -43,6 +44,32 @@ struct StructuredOpInterface
     auto zero = arith::ConstantIndexOp::create(builder, loc, 0);
     auto one = arith::ConstantIndexOp::create(builder, loc, 1);
 
+    Value iterationDomainIsNonDegenerate;
+    for (auto [start, end] : llvm::zip(starts, ends)) {
+      auto startValue = getValueOrCreateConstantIndexOp(builder, loc, start);
+      auto endValue = getValueOrCreateConstantIndexOp(builder, loc, end);
+
+      // Loop Trip count > 0 iff start < end
+      Value dimensionHasNonZeroTripCount = builder.create<index::CmpOp>(
+          loc, index::IndexCmpPredicate::SLT, startValue, endValue);
+
+      if (!iterationDomainIsNonDegenerate) {
+        iterationDomainIsNonDegenerate = dimensionHasNonZeroTripCount;
+      } else {
+        // Iteration domain is non-degenerate iff all dimensions have loop trip
+        // count > 0
+        iterationDomainIsNonDegenerate = builder.create<arith::AndIOp>(
+            loc, iterationDomainIsNonDegenerate, dimensionHasNonZeroTripCount);
+      }
+    }
+
+    if (!iterationDomainIsNonDegenerate)
+      return;
+
+    auto ifOp = builder.create<scf::IfOp>(loc, iterationDomainIsNonDegenerate,
+                                          /*withElseRegion=*/false);
+    builder.setInsertionPointToStart(&ifOp.getThenRegion().front());
+
     // Subtract one from the loop ends before composing with the indexing map
     transform(ends, ends.begin(), [&](OpFoldResult end) {
       auto endValue = getValueOrCreateConstantIndexOp(builder, loc, end);
@@ -110,11 +137,11 @@ struct StructuredOpInterface
         builder.createOrFold<cf::AssertOp>(loc, cmpOp, msg);
       }
     }
+    builder.setInsertionPointAfter(ifOp);
   }
 };
 
-template <typename... OpTs>
-void attachInterface(MLIRContext *ctx) {
+template <typename... OpTs> void attachInterface(MLIRContext *ctx) {
   (OpTs::template attachInterface<StructuredOpInterface<OpTs>>(*ctx), ...);
 }
 } // namespace
diff --git a/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir b/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir
index 9f4393efc87bf..e48dcbd6c6110 100644
--- a/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir
+++ b/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir
@@ -103,6 +103,11 @@ func.func @main() {
   // CHECK: unexpected negative result on dimension #0 of input/output operand #0
   func.call @reverse_from_3(%d5x) : (tensor<?xf32>) -> (tensor<?xf32>)
 
+  %c0x = arith.constant dense<1.0> : tensor<0xf32>
+  %d0x = tensor.cast %c0x : tensor<0xf32> to tensor<?xf32>
+  // CHECK-NOT: ERROR: Runtime op verification failed
+  func.call @fill_empty_1d(%d0x) : (tensor<?xf32>) -> (tensor<?xf32>)
+
   return
 }
 
@@ -297,3 +302,9 @@ func.func @reverse_from_3(%arg0: tensor<?xf32>) -> (tensor<?xf32>) {
   } -> tensor<?xf32>
   return %result : tensor<?xf32>
 }
+
+func.func @fill_empty_1d(%arg0: tensor<?xf32>) -> (tensor<?xf32>) {
+  %c0 = arith.constant 0.0 : f32
+  %0 = linalg.fill ins(%c0 : f32) outs(%arg0 : tensor<?xf32>) -> tensor<?xf32>
+  return %0 : tensor<?xf32>
+}

@llvmbot
Copy link
Member

llvmbot commented Oct 16, 2025

@llvm/pr-subscribers-mlir

Author: Hanumanth (Hanumanth04)

Changes

[mlir][linalg] Fix Linalg runtime verification pass to handle tensors with dimensions of size 0

Runtime verification on Linalg structured ops unconditionally computed end - 1 to determine the last iteration index before composing indexing maps. This caused spurious "negative index" assertion failures while operating on empty tensors (tensors with a dimension of size 0).

The issue occurs because:

  1. Empty tensors create loop ranges [0, 0) with zero trip count

  2. Computing end - 1 = 0 - 1 = -1 creates a fictitious negative index

  3. The negative index check triggers even though no loop iterations occur

The fix is to guard all runtime verification with a check that ensures all loop ranges are non-empty (start < end) before performing any index arithmetic.

Example MLIR that previously failed:

func.func @<!-- -->fill_empty() -&gt; tensor&lt;0xi32&gt; {
  %c0 = arith.constant 0 : i32
  %empty = tensor.empty() : tensor&lt;0xi32&gt;
  %filled = linalg.fill ins(%c0 : i32) outs(%empty : tensor&lt;0xi32&gt;) -&gt; tensor&lt;0xi32&gt;
  return %filled : tensor&lt;0xi32&gt;
}

Full diff: https://github.com/llvm/llvm-project/pull/163791.diff

2 Files Affected:

  • (modified) mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp (+29-2)
  • (modified) mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir (+11)
diff --git a/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp b/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp
index 15eb51a6dcab2..737652c8cb9d1 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/RuntimeOpVerification.cpp
@@ -17,6 +17,7 @@
 #include "mlir/Dialect/Index/IR/IndexOps.h"
 #include "mlir/Dialect/Linalg/IR/Linalg.h"
 #include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/SCF/IR/SCF.h"
 #include "mlir/Dialect/Tensor/IR/Tensor.h"
 #include "mlir/Interfaces/RuntimeVerifiableOpInterface.h"
 
@@ -43,6 +44,32 @@ struct StructuredOpInterface
     auto zero = arith::ConstantIndexOp::create(builder, loc, 0);
     auto one = arith::ConstantIndexOp::create(builder, loc, 1);
 
+    Value iterationDomainIsNonDegenerate;
+    for (auto [start, end] : llvm::zip(starts, ends)) {
+      auto startValue = getValueOrCreateConstantIndexOp(builder, loc, start);
+      auto endValue = getValueOrCreateConstantIndexOp(builder, loc, end);
+
+      // Loop Trip count > 0 iff start < end
+      Value dimensionHasNonZeroTripCount = builder.create<index::CmpOp>(
+          loc, index::IndexCmpPredicate::SLT, startValue, endValue);
+
+      if (!iterationDomainIsNonDegenerate) {
+        iterationDomainIsNonDegenerate = dimensionHasNonZeroTripCount;
+      } else {
+        // Iteration domain is non-degenerate iff all dimensions have loop trip
+        // count > 0
+        iterationDomainIsNonDegenerate = builder.create<arith::AndIOp>(
+            loc, iterationDomainIsNonDegenerate, dimensionHasNonZeroTripCount);
+      }
+    }
+
+    if (!iterationDomainIsNonDegenerate)
+      return;
+
+    auto ifOp = builder.create<scf::IfOp>(loc, iterationDomainIsNonDegenerate,
+                                          /*withElseRegion=*/false);
+    builder.setInsertionPointToStart(&ifOp.getThenRegion().front());
+
     // Subtract one from the loop ends before composing with the indexing map
     transform(ends, ends.begin(), [&](OpFoldResult end) {
       auto endValue = getValueOrCreateConstantIndexOp(builder, loc, end);
@@ -110,11 +137,11 @@ struct StructuredOpInterface
         builder.createOrFold<cf::AssertOp>(loc, cmpOp, msg);
       }
     }
+    builder.setInsertionPointAfter(ifOp);
   }
 };
 
-template <typename... OpTs>
-void attachInterface(MLIRContext *ctx) {
+template <typename... OpTs> void attachInterface(MLIRContext *ctx) {
   (OpTs::template attachInterface<StructuredOpInterface<OpTs>>(*ctx), ...);
 }
 } // namespace
diff --git a/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir b/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir
index 9f4393efc87bf..e48dcbd6c6110 100644
--- a/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir
+++ b/mlir/test/Integration/Dialect/Linalg/CPU/runtime-verification.mlir
@@ -103,6 +103,11 @@ func.func @main() {
   // CHECK: unexpected negative result on dimension #0 of input/output operand #0
   func.call @reverse_from_3(%d5x) : (tensor<?xf32>) -> (tensor<?xf32>)
 
+  %c0x = arith.constant dense<1.0> : tensor<0xf32>
+  %d0x = tensor.cast %c0x : tensor<0xf32> to tensor<?xf32>
+  // CHECK-NOT: ERROR: Runtime op verification failed
+  func.call @fill_empty_1d(%d0x) : (tensor<?xf32>) -> (tensor<?xf32>)
+
   return
 }
 
@@ -297,3 +302,9 @@ func.func @reverse_from_3(%arg0: tensor<?xf32>) -> (tensor<?xf32>) {
   } -> tensor<?xf32>
   return %result : tensor<?xf32>
 }
+
+func.func @fill_empty_1d(%arg0: tensor<?xf32>) -> (tensor<?xf32>) {
+  %c0 = arith.constant 0.0 : f32
+  %0 = linalg.fill ins(%c0 : f32) outs(%arg0 : tensor<?xf32>) -> tensor<?xf32>
+  return %0 : tensor<?xf32>
+}

@Hanumanth04 Hanumanth04 changed the title Bug fix linalg runtime verification pass [mlir][linalg] Fix Linalg runtime verification pass to handle tensors with dimensions of size 0 Oct 16, 2025
@github-actions
Copy link

github-actions bot commented Oct 16, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@Hanumanth04
Copy link
Contributor Author

Tagging @ryanpholt, could you please take a look?

@Hanumanth04 Hanumanth04 force-pushed the bugFixLinalgRuntimeVerificationPass branch from 7f694ee to f658bd3 Compare October 16, 2025 14:20
@rengolin rengolin requested a review from banach-space October 16, 2025 14:40
@ryanpholt ryanpholt self-requested a review October 16, 2025 18:12
Copy link
Member

@ryanpholt ryanpholt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me -- thanks!

@Hanumanth04 Hanumanth04 force-pushed the bugFixLinalgRuntimeVerificationPass branch from f658bd3 to 43f73d7 Compare October 20, 2025 21:06
@Hanumanth04
Copy link
Contributor Author

Hi @ryanpholt , @matthias-springer

I don't have permission to merge the change. Could you please help merge this PR when you get a chance?

Thanks!

@matthias-springer matthias-springer merged commit d08cbc1 into llvm:main Oct 22, 2025
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants